﻿/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Net.Sockets;
using System.IO;
using System.Text;
using System.Threading;
using System.Reflection;
using EffiProz.Core;
using EffiProz.Core.Lib;
using EffiProz.Core.RowIO;
using EffiProz.Core.Resources;
using EffiProz.Core.Persist;
using EffiProz.Core.Lib.IO;
using EffiProz.Core.Results;
using EffiProz.Core.Errors;




namespace EffiProz.Server
{
    class WebServerConnection
    {
        const int HTTP_BAD_REQUEST = 400;
        const int HTTP_NOT_FOUND = 404;
        const int HTTP_FORBIDDEN = 403;
        static Encoding ENCODING = Encoding.GetEncoding("iso-8859-1");
        private TcpClient socket;
        private WebServer server;
        private const int REQUEST_TYPE_BAD = 0;
        private const int REQUEST_TYPE_GET = 1;
        private const int REQUEST_TYPE_HEAD = 2;
        private const int REQUEST_TYPE_POST = 3;
        private const String HEADER_OK = "HTTP/1.0 200 OK";
        private const String HEADER_BAD_REQUEST =
            "HTTP/1.0 400 Bad Request";
        private const String HEADER_NOT_FOUND = "HTTP/1.0 404 Not Found";
        private const String HEADER_FORBIDDEN = "HTTP/1.0 403 Forbidden";
        const int BUFFER_SIZE = 256;
       
        private RowOutputBinary rowOut = new RowOutputBinary(new byte[BUFFER_SIZE]);
        private RowInputBinary rowIn;

        //
        static byte[] BYTES_GET = Encoding.ASCII.GetBytes("GET");
        static byte[] BYTES_HEAD = Encoding.ASCII.GetBytes("HEAD");
        static byte[] BYTES_POST = Encoding.ASCII.GetBytes("POST");
        static byte[] BYTES_CONTENT = Encoding.ASCII.GetBytes("Content-Length: ");
        static byte[] BYTES_WHITESPACE = new byte[] {
        (byte) ' ', (byte) '\t'
    };

        private Stream serverStream = null;

        // default mime type mappings
        private static int hnd_content_types =
            BundleHandler.getBundleHandle("EffiProz.Server.Resources.content-types", 
            typeof(WebServerConnection).Assembly);

        /**
         * Creates a new WebServerConnection to the specified WebServer on the
         * specified socket.
         *
         * @param socket the network socket on which WebServer communication
         *      takes place
         * @param server the WebServer instance to which the object
         *      represents a connection
         */
        public WebServerConnection(TcpClient socket, WebServer server)
        {
            this.server = server;
            this.socket = socket;
            rowIn = new RowInputBinary(rowOut);            
        }

        /**
         * Retrieves a best-guess mime-type string using the file extension
         * of the name argument.
         *
         * @return a best-guess mime-type string using the file extension
         *      of the name argument.
         */
        private String getMimeTypeString(String name)
        {

            int pos;
            String key;
            String mimeType;

            if (name == null)
            {
                return ServerConstants.SC_DEFAULT_WEB_MIME;
            }

            pos = name.LastIndexOf('.');
            key = null;
            mimeType = null;

            // first search user-specified mapping
            if (pos >= 0)
            {
                key = name.Substring(pos).ToLower();
                mimeType = server.serverProperties.getProperty(key);
            }

            // if not found, search default mapping
            if (mimeType == null && key.Length > 1)
            {
                mimeType = BundleHandler.getString(hnd_content_types,
                                                   key.Substring(1));
            }

            return mimeType == null ? ServerConstants.SC_DEFAULT_WEB_MIME
                                    : mimeType;
        }

        /**
         * Causes this WebServerConnection to process its HTTP request
         * in a blocking fashion until the request is fully processed
         * or an exception occurs internally.
         *
         * This method reads the Request line then delegates action to subroutines.
         */
        public void run()
        {
            DataInputStream inStream = null;
            try
            {
                serverStream = ServerConnection.GetServerStream(server.isTls(), socket, server.ServerCertificate);

                inStream =
                  new DataInputStream(serverStream);
                int count;               
                String name = null;
                int method = REQUEST_TYPE_BAD;
            
                // read line, ignoring any leading blank lines
                do
                {
                    count = InOutUtil.readLine(inStream, rowOut);

                    if (count == 0)
                    {
                        throw new Exception();
                    }
                } while (count < 2);

                byte[] byteArray = rowOut.getBuffer();
                int offset = rowOut.size() - count;

                if (ArrayUtil.containsAt(byteArray, offset, BYTES_POST))
                {
                    method = REQUEST_TYPE_POST;
                    offset += BYTES_POST.Length;
                }
                else if (ArrayUtil.containsAt(byteArray, offset, BYTES_GET))
                {
                    method = REQUEST_TYPE_GET;
                    offset += BYTES_GET.Length;
                }
                else if (ArrayUtil.containsAt(byteArray, offset, BYTES_HEAD))
                {
                    method = REQUEST_TYPE_HEAD;
                    offset += BYTES_HEAD.Length;
                }
                else
                {
                    method = REQUEST_TYPE_BAD;
                }

                count = ArrayUtil.countStartElementsAt(byteArray, offset,
                                                       BYTES_WHITESPACE);

                if (count == 0)
                {
                    method = REQUEST_TYPE_BAD;
                }

                offset += count;
                count = ArrayUtil.countNonStartElementsAt(byteArray, offset,
                        BYTES_WHITESPACE);
                name = ENCODING.GetString(byteArray, offset, count);

                switch (method)
                {

                    case REQUEST_TYPE_BAD:
                        processError(REQUEST_TYPE_BAD);
                        break;

                    case REQUEST_TYPE_GET:
                        processGet(name, true);
                        break;

                    case REQUEST_TYPE_HEAD:
                        processGet(name, false);
                        break;

                    case REQUEST_TYPE_POST:
                        processPost(inStream, name);
                        break;
                }

                inStream.Close();
                socket.Close();
            }
            catch (Exception e)
            {
                server.printStackTrace(e);
            }
            finally
            {
                try
                {
                    if (inStream != null)
                    {
                        inStream.Close();
                    }

                    socket.Close();
                }
                catch (IOException ioe)
                {
                    server.printStackTrace(ioe);
                }
            }
        }

        /**
         * POST is used only for database access. So we can assume the strings
         * are those generated by HTTPClientConnection
         */
        private void processPost(Stream inStream,
                                 String name)
        {

            // fredt - parsing in this block is not actually necessary
            try
            {

                // read the Content-Type line
                InOutUtil.readLine(inStream, rowOut);

                // read and parse the Content-Length line
                int count = InOutUtil.readLine(inStream, rowOut);
                int offset = rowOut.size() - count;

                // get buffer always after reading into rowOut, else old buffer may
                // be returned
                byte[] byteArray = rowOut.getBuffer();

                if (!ArrayUtil.containsAt(byteArray, offset, BYTES_CONTENT))
                {
                    throw new Exception();
                }

                count -= BYTES_CONTENT.Length;
                offset += BYTES_CONTENT.Length;

                // omit the last two characters
                String lenStr = ENCODING.GetString(byteArray, offset, count - 2);
                int length = int.Parse(lenStr);

                InOutUtil.readLine(inStream, rowOut);
            }
            catch (Exception )
            {
                processError(HTTP_BAD_REQUEST);

                return;
            }

            processQuery(inStream);
        }

        /**
         * Processes a database query in HSQL protocol that has been
         * tunneled over HTTP protocol.
         *
         * @param inStream the incoming byte stream representing the HSQL protocol
         *      database query
         */
        void processQuery(Stream inStream)
        {

            try
            {
                DataInputStream dataIn = new DataInputStream(inStream);

                int databaseID = dataIn.ReadInt();
                long sessionID = dataIn.ReadLong();
                int mode = dataIn.ReadByte();
                Session session = DatabaseManager.getSession(databaseID,
                    sessionID);
                Result resultIn = Result.newResult(session, mode, dataIn, rowIn);

                resultIn.setDatabaseId(databaseID);
                resultIn.setSessionId(sessionID);          

                //
                Result resultOut;

                if (resultIn.getType() == ResultConstants.CONNECT)
                {
                    try
                    {
                        String databaseName = resultIn.getDatabaseName();
                        int dbIndex = server.getDBIndex(databaseName);
                        int dbID = server.dbID[dbIndex];

                        session = DatabaseManager.newSession(dbID,
                            resultIn.getMainString(), resultIn.getSubString(), resultIn.getZoneString(),
                            resultIn.getUpdateCount());

                        resultIn.readAdditionalResults(session, dataIn, rowIn);

                        resultOut = Result.newConnectionAcknowledgeResponse(
                            session.getId(), dbID);
                    }
                    catch (CoreException e)
                    {
                        resultOut = Result.newErrorResult(e);
                    }
                    catch (Exception e)
                    {
                        resultOut = Result.newErrorResult(e);
                    }
                }
                else
                {
                    int dbID = resultIn.getDatabaseId();

                    if (session == null)
                    {
                        resultOut = Result.newErrorResult(
                            Error.error(ErrorCode.SERVER_DATABASE_DISCONNECTED));
                    }
                    else
                    {
                        resultIn.setSession(session);
                        resultIn.readAdditionalResults(session, dataIn, rowIn);

                        resultOut = session.execute(resultIn);
                    }
                }

                int type = resultIn.getType();

                if (type == ResultConstants.DISCONNECT
                        || type == ResultConstants.RESETSESSION)
                {
                    return;
                }

                Stream outStream = serverStream;

                String header = getHead(HEADER_OK, false,
                                        "application/octet-stream", rowOut.size());

                outStream.Write(ENCODING.GetBytes(header), 0, ENCODING.GetByteCount(header));
                outStream.Flush();
                resultOut.write(outStream, rowOut);
                outStream.Close();
            }
            catch (Exception e)
            {
                server.printStackTrace(e);
            }
        }

        /**
         *  Processes an HTTP GET request
         *
         * @param  name the name of the content to get
         * @param  send whether to send the content as well, or just the header
         */
        private void processGet(String name, bool send)
        {

            try
            {
                String hdr;
                Stream os;
                Stream ips;
                int b;

                if (name.EndsWith("/"))
                {
                    name = name + server.getDefaultWebPage();
                }

                // traversing up the directory structure is forbidden.
                if (name.IndexOf("..") != -1)
                {
                    processError(HTTP_FORBIDDEN);

                    return;
                }

                name = server.getWebRoot() + name;

                if (Path.PathSeparator != '/')
                {
                    name = name.Replace('/', Path.PathSeparator);
                }

                ips = null;

                server.printWithThread("GET " + name);

                try
                {
                    FileInfo file = new FileInfo(name);

                    ips = file.OpenRead();
               
                    hdr = getHead(HEADER_OK, true, getMimeTypeString(name),
                                  (int)file.Length);
                }
                catch (IOException)
                {
                    processError(HTTP_NOT_FOUND);

                    if (ips != null)
                    {
                        ips.Close();
                    }

                    return;
                }

                os = serverStream;

                os.Write(ENCODING.GetBytes(hdr), 0, ENCODING.GetByteCount(hdr));

                if (send)
                {
                    byte[] buffer = new byte[512];

                    while ((b = ips.Read(buffer, 0, 512)) != -1)
                    {
                        os.Write(buffer, 0, b);
                    }
                }

                os.Flush();
                os.Close();
                ips.Close();
            }
            catch (Exception e)
            {
                server.printError("processGet: " + e.ToString());
                server.printStackTrace(e);
            }
        }

        /**
         * Retrieves an HTTP protocol header given the supplied arguments.
         *
         * @param responseCodeString the HTTP response code
         * @param addInfo true if additional header info is to be added
         * @param mimeType the Content-Type field value
         * @param length the Content-Length field value
         * @return an HTTP protocol header
         */
        String getHead(String responseCodeString, bool addInfo,
                       String mimeType, int length)
        {

            StringBuilder sb = new StringBuilder(128);

            sb.Append(responseCodeString).Append("\r\n");

            if (addInfo)
            {
                sb.Append("Allow: GET, HEAD, POST\nMIME-Version: 1.0\r\n");
                sb.Append("Server: ").Append(
                    EffiProzDatabaseProperties.PRODUCT_NAME).Append("\r\n");
            }

            if (mimeType != null)
            {
                sb.Append("Content-Type: ").Append(mimeType).Append("\r\n");
                sb.Append("Content-Length: ").Append(length).Append("\r\n");
            }

            sb.Append("\r\n");

            return sb.ToString();
        }

        /**
         *  Processess an HTTP error condition, sending an error response to
         *  the client.
         *
         * @param code the error condition code
         */
        private void processError(int code)
        {

            String msg;

            server.printWithThread("processError " + code);

            switch (code)
            {

                case HTTP_BAD_REQUEST:
                    msg = getHead(HEADER_BAD_REQUEST, false, null, 0);
                    msg +=  BundleHandler.getString(WebServer.webBundleHandle,
                                                   "BAD_REQUEST");
                    break;

                case HTTP_FORBIDDEN:
                    msg = getHead(HEADER_FORBIDDEN, false, null, 0);
                    msg += BundleHandler.getString(WebServer.webBundleHandle,
                                                   "FORBIDDEN");
                    break;

                case HTTP_NOT_FOUND:
                default:
                    msg = getHead(HEADER_NOT_FOUND, false, null, 0);
                    msg += BundleHandler.getString(WebServer.webBundleHandle,
                                                   "NOT_FOUND");
                    break;
            }

            try
            {
                Stream os = serverStream;            

                os.Write(ENCODING.GetBytes(msg), 0, ENCODING.GetByteCount(msg));
                os.Flush();
                os.Close();
            }
            catch (Exception e)
            {
                server.printError("processError: " + e.ToString());
                server.printStackTrace(e);
            }
        }

        /**
         * Retrieves the thread name to be used  when
         * this object is the Runnable object of a Thread.
         *
         * @return the thread name to be used  when
         *      this object is the Runnable object of a Thread.
         */
        public String getConnectionThreadName()
        {
            return "EffiProz HTTP Connection @" + string.Format("{0:x2}", this.GetHashCode());
        }
    }
}
